package zorm

/*
	所有的字段都在这个表中体现
	并且表字段可以与结构同步，也可以完全独立（脱离系统存在）
	系统运行时，将会载入所有字段，与结构相比较，如果不存在，则添加到表中
	结构中包含结构时，必须使用非指针类型
	数据库取出时，将自动将ID给many2one结构，但只是赋ID值，其它值，需要手工读取
	many2many类型为jsonb类型
	强制为MYSQL支持，不支持其它数据库
*/
import (
	"database/sql"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"time"

	_ "github.com/go-sql-driver/mysql" // import your used driver
)

const (
	TYPE_BOOL       = 1 //真假类型
	TYPE_INT        = 2 //数字类型
	TYPE_STRING     = 3 //字符串类型
	TYPE_FLOAT64    = 4
	TYPE_ENUM       = 5 //枚举类型
	TYPE_MANY2ONE   = 6 //多对一
	TYPE_MANY2MANY  = 7 //多对多
	TYPE_JSON       = 8 //JSON数据 实现NOSQL
	TYPE_DATETIME   = 9
	TYPE_INT64      = 10 //数字类型
	TYPE_UINT8      = 11
	TYPE_EMAIL      = 12 //邮件类型
	TYPE_TABLEFIELD = 13 //其它表的字段
	TYPE_UINT16     = 14
	TYPE_INT32      = 15
	TYPE_INT16      = 16
	//TYPE_OBJECT     = 14 //任意类型，将会被JSON转为字符串
)

var (
	_tp               []interface{}
	ErrRecordNotFound = errors.New("record not found")
)

func init() {
	_tp = append(_tp, nil)        //0
	_tp = append(_tp, true)       //1
	_tp = append(_tp, int(0))     //2
	_tp = append(_tp, "")         //3
	_tp = append(_tp, float64(0)) //4
	_tp = append(_tp, int(0))     //5
	_tp = append(_tp, int(0))     //6
	_tp = append(_tp, "")         //7
	_tp = append(_tp, "")         //8
	_tp = append(_tp, time.Now()) //9
	_tp = append(_tp, int64(0))   //10
	_tp = append(_tp, uint8(0))   //11
	_tp = append(_tp, "")         //12
	_tp = append(_tp, "")         //13
	_tp = append(_tp, "")         //14
	_tp = append(_tp, int32(0))   //15
	_tp = append(_tp, int16(0))   //16
}

/*
	此系统支持数据类型
	orm类型		golang类型		数据库类型
	-----------------------------------------
	bool          bool             bool
	int			  int			   int
	string		  string		   string
	float         float            float
	//[]json		  []类型/[]*类型    jsonb
	//json          类型/*类型        jsonb
	enum		  int  			   int
	many2one      类型/*类型        int
	many2many     []类型            string
*/
type SFields struct {
	Id        int         //ID
	Name      string      //在golang中的字段名称，区分大小写
	Uid       int         //哪个用户使用的，如果是0，则是系统使用
	Model     string      //模块，这个模块是谁用的
	Field     string      //字段变量名称
	Type      int         //类型  参考 系统支持数据类型
	Relation  string      //关联类,字段强制为ID
	TypeObj   interface{} //类型模版
	Default   string      //默认值 非string时,将做转换,必须转换成功
	Offset    uintptr     //相对于起始地址，用于加速读取，和实现所有字段对外显示
	IndexName string      //索引名称
	//IsJson   bool    //是否是Json字段
}
type ZDB struct {
	Db        *sql.DB //数据库句柄
	isDebug   bool    //是否调试模式
	dbtype    string
	dbconnstr string
	dbname    string
}

func (t *SFields) TableName() string {
	return "s_fields"
}
func (t *SFields) New() interface{} {
	return new(SFields)
}

// 返回可以操作的安全字段列表
func (t *SFields) GetSafeFields(uid int) []string {
	return []string{}
}

//检测字段是否正确
func (t *SFields) Scan(uid int) error {
	var ret string
	if len(ret) != 0 {
		return errors.New(ret)
	}
	return nil
}

//功能 查找指定模块下是否包含指定字段
//参数
//     table: 表名
//     field: 字段名
//返回
//     真/假
func GetModelField(table string, field string) (*SFields, error) {

	table = strings.ToLower(table)

	t, ok := fmTables[table]
	if ok == false {
		panic("GetModelField-未发现模块:" + table)
	}
	t1, ok := t.Fields[field]
	if ok == false {
		//return nil, errors.New("未发现字段:" + field)
		panic(fmt.Sprintf("表%s未发现字段:%s", table, field))
	}
	return t1, nil
}

// 根据表名，获取对应的实例
func GetTable(table string) (interface{}, error) {
	table = strings.ToLower(table)
	t, ok := fmTables[table]
	if ok == false {
		return nil, errors.New("未发现表:" + table)
	}
	return t.YZ, nil
}

//功能 返回一个新实例
func (z *ZDB) New() *ZDB {
	return &ZDB{Db: z.Db, dbtype: z.dbtype, dbconnstr: z.dbconnstr}
}
func (z *ZDB) Debug() *ZDB {
	z.isDebug = true
	return z
}

//功能 通过字符串生成指定类型的变量
func GetType(tp int) interface{} {
	if tp >= len(_tp) {
		panic("非法类型")
	}
	if tp == 0 {
		panic("不能为0!")
	}
	return reflect.New(reflect.TypeOf(_tp[tp])) //生成新的实例
}

//功能 将搜索到的数据，转换成结构数据
//参数
func Rows2Map(rows *sql.Rows, table string) ([]map[string]interface{}, error) {
	cols, err := rows.Columns() //获取列名称
	if err != nil {
		return nil, err
	}
	var vals []interface{} //赋值
	var tps []*SFields
	for _, v := range cols { //遍历结果
		row, err := GetModelField(table, v)
		if row != nil {
			return nil, err
		}
		obj := GetType(row.Type)
		vals = append(vals, &obj) //添加到列表中
		tps = append(tps, row)
	}
	var ret []map[string]interface{}

	for rows.Next() {
		if err := rows.Scan(vals...); err != nil {
			return nil, err
		}
		m := make(map[string]interface{}) //申请MAP
		for i, v := range cols {          //将数据转换成map
			m[v], err = ConvInterface2Type(*(vals[i].(*interface{})), tps[i])
			if err != nil {
				return nil, err
			}
		}
		ret = append(ret, m)
	}
	return ret, nil
}

//功能 将切片map转换成struct数据
//返回 成功/失败
func map2Struct(m map[string]interface{}, obj interface{}) {
	minfo, err := GetTableInfo(obj)
	if err != nil {
		panic(err)
	}
	decobj := reflect.ValueOf(obj)
	for decobj.Kind() == reflect.Ptr {
		decobj = decobj.Elem()
	}
	//在这里先获取obj类型的名称
	for k, v := range m {
		fs, ok := minfo.Fields[k] //获取字段类型
		if ok == false {
			panic("未发现的字段:" + k)
		}
		fmt.Printf("MAP2STRUCT 字段:%s 源数据类型:%T %v,目标类型:%T,%v\r\n", k, v, v, fs.TypeObj, fs.TypeObj)
		obj2, err := ConvInterface2Type(v, fs.TypeObj) //  类型转换
		if err != nil {
			panic(err)
		}
		vv := decobj.FieldByName(k)
		vv.Set(reflect.ValueOf(obj2))
	}
}

//所有的数据库全在这里
var allDB map[string]*ZDB

//功能 打开一个指定的数据库
//返回 数据库句柄
func OpenDB(sqltype string, sqlconnstr string, namespace string, isdebug bool) error {
	ret := &ZDB{}
	db, err := sql.Open(sqltype, sqlconnstr)
	if err != nil {
		return err
	}
	if err := db.Ping(); err != nil {
		return err
	}
	//分析出来数据库的名称
	//root:@tcp(127.0.0.1:3306)/zerp?parseTime=true&charset=utf8&loc=Local&parseTime=true
	switch sqltype {
	case "mysql":
		sp := strings.Split(sqlconnstr, "/")
		if len(sp) == 1 {
			panic("不支持的连接类型")
		}
		ret.dbname = strings.Split(sp[1], "?")[0] //获取名称
	case "postgres":
		sp := strings.Split(sqlconnstr, " ")
		for _, v := range sp {
			sp1 := strings.Split(v, "=")
			if strings.ToLower(sp1[0]) == "dbname" {
				ret.dbname = sp1[1] //获取名称
				break
			}
		}
	default:
		panic("不支持的连接类型")
	}
	db.SetMaxOpenConns(2000)
	db.SetMaxIdleConns(1000)
	ret.dbtype = sqltype
	ret.dbconnstr = sqlconnstr
	ret.Db = db
	if allDB == nil {
		allDB = make(map[string]*ZDB)
	}
	if namespace == "" { //设置默认数据名称
		namespace = "default"
	}
	if _, ok := allDB[namespace]; ok == true {
		panic("已经存在的实例:" + namespace)
	}
	if isdebug == true {
		ret.Debug()
	}
	allDB[namespace] = ret
	return nil
}
func GetDB(namespace string) *ZDB {
	if len(namespace) == 0 {
		namespace = "default"
	}
	if allDB == nil {
		panic("请先使用OpenDB打开数据库")
	}
	ret, ok := allDB[namespace]
	if ok == false {
		panic("不存在的数据库定义:" + namespace)
	}
	if ret == nil {
		panic("返回了为空的命名空间:" + namespace)
	}

	if (ret.dbtype == "mysql") && (ret.HasTable(&SUser{}) == false) { //如果不存在系统表，则创建系统表
		if err := ret.CreateTable(&SAccess{}); err != nil { //创建zorm基本表
			panic(err)
		}
		if err := ret.CreateTable(&SRule{}); err != nil {
			panic(err)
		}
		if err := ret.CreateTable(&SUser{}); err != nil {
			panic(err)
		}
		if err := ret.CreateTable(&SGroup{}); err != nil {
			panic(err)
		}
		/*if err := ret.CreateTable(&SFields{}); err != nil {
			panic(err)
		}*/

		sql := "INSERT INTO s_group (id,name)value(1,'系统管理员')"
		if _, err := ret.Db.Exec(sql); err != nil {
			panic(err)
		}
		sql = "INSERT INTO s_group (id,name)value(2,'普通人员')"
		if _, err := ret.Db.Exec(sql); err != nil {
			panic(err)
		}
		sql = "INSERT INTO s_user (umail,upass,isuse,groups)value('test@qq.com','123456',1,',2,')"
		if _, err := ret.Db.Exec(sql); err != nil {
			panic(err)
		}
		sql = "INSERT INTO s_user (umail,upass,isuse,groups)value('admin@qq.com','123456',1,',1,2,')"
		if _, err := ret.Db.Exec(sql); err != nil {
			panic(err)
		}
	}
	return ret
}

//功能 关闭数据
//注意 无
func (c *ZDB) Close() {
	c.Db.Close()
}
